﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Markup;
using Relay.UI.Controls.Docking.Primitives;
using System.Collections.Specialized;
using System.Diagnostics;
using Relay.UI.Controls.Utility;

namespace Relay.UI.Controls.Docking {
    /// <summary>
    /// A visual area in which movable panes can be docked.
    /// </summary>
    [ContentProperty("SplitPanels")]
    public class DockManager : Control, IFrameworkElementHost, IDockHierarchyObjectHost, IDropTarget {
        /// <summary>
        /// The minimum size for any given cell area in a DockManager.
        /// </summary>
        public const double MinimumAreaSize = 24.0;

        /// <summary>
        /// The width of a splitter.
        /// </summary>
        public const double SplitterWidth = 4.0;

        #region Fields
        private BoundFrameworkElementCollection _splitPanels = null;
        private FrameworkElement _childElement = null;
        private Rectangle _backgroundRect = new Rectangle();
        private DockManagerSplitter[] _splitters = new DockManagerSplitter[0];
        private Rect _contentRect = Rect.Empty;
        private bool _resynchronizing = false;
        private bool _isDirty = true;
        private int _collapsedChildren = 0;
        private DockOperationService _dockService = null;
        private List<FloatingWindow> _floatingWindows = new List<FloatingWindow>();
        #endregion

        #region Events
        internal event EventHandler FocusedWindowGroupChanged;
        #endregion

        #region Dependency Properties
        /// <summary>
        /// Identifies the FocusedWindowGroup dependency property.
        /// </summary>
        public static readonly DependencyProperty FocusedWindowGroupProperty = DependencyProperty.Register(
            "FocusedWindowGroup", typeof(DockWindowGroup), typeof(DockManager));

        /// <summary>
        /// Identifies the Dock dependency property.
        /// </summary>
        public static readonly DependencyProperty DockProperty = DependencyProperty.RegisterAttached("Dock", typeof(Dock),
            typeof(DockManager), new FrameworkPropertyMetadata(Dock.Left,
                FrameworkPropertyMetadataOptions.AffectsParentMeasure));

        /// <summary>
        /// Gets the value of the Dock dependency property for the specified UIElement.
        /// </summary>
        /// <param name="element">The element from which the property value is read.</param>
        /// <returns>The Dock property for the element.</returns>
        public static Dock GetDock(UIElement element) {
            return (Dock)element.GetValue(DockProperty);
        }

        /// <summary>
        /// Sets the value of the Dock dependency property for the specified UIElement.
        /// </summary>
        /// <param name="element">The element to which the attached property is written.</param>
        /// <param name="value">The needed Dock value.</param>
        public static void SetDock(UIElement element, Dock value) {
            element.SetValue(DockProperty, value);
        }

        /// <summary>
        /// Identifies the CellSize dependency property.
        /// </summary>
        public static readonly DependencyProperty CellSizeProperty = DependencyProperty.RegisterAttached("CellSize",
            typeof(double), typeof(DockManager), new FrameworkPropertyMetadata(100.0,
                FrameworkPropertyMetadataOptions.AffectsParentMeasure));

        /// <summary>
        /// Gets the value of the CellSize dependency property for the specified UIElement.
        /// </summary>
        /// <param name="element">The element from which the property value is read.</param>
        /// <returns>The CellSize property for the element.</returns>
        public static double GetCellSize(UIElement element) {
            return (double)element.GetValue(CellSizeProperty);
        }

        /// <summary>
        /// Sets the value of the CellSize dependency property for the specified UIElement.
        /// </summary>
        /// <param name="element">The element to which the attached property is written.</param>
        /// <param name="value">The needed CellSize value.</param>
        public static void SetCellSize(UIElement element, double value) {
            element.SetValue(CellSizeProperty, value);
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Initializes the <see cref="DockManager"/> class.
        /// </summary>
        static DockManager() {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(DockManager),
                new FrameworkPropertyMetadata(typeof(DockManager)));

            UIElement.FocusableProperty.OverrideMetadata(typeof(DockManager), new FrameworkPropertyMetadata(false));
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DockManager"/> class.
        /// </summary>
        public DockManager() {
            _splitPanels = new BoundFrameworkElementCollection(this, new Type[] { typeof(SplitPanel) });
            _backgroundRect.SetResourceReference(Rectangle.FillProperty, SystemColors.AppWorkspaceBrushKey);
            AddVisualChild(_backgroundRect);

            _dockService = new DockOperationService(this);
        }
        #endregion
        
        #region Properties
        /// <summary>
        /// Gets or sets the child element of this dock manager.
        /// </summary>
        /// <value>The child element.</value>
        public FrameworkElement ChildElement {
            get { return _childElement; }
            set { _childElement = value; }
        }
        
        /// <summary>
        /// Gets the dockable children of this control.
        /// </summary>
        /// <value>The collection of split panels.</value>
        public BoundFrameworkElementCollection SplitPanels {
            get { return _splitPanels; }
        }

        /// <summary>
        /// Gets or sets the area of the dock manager that is unused by docked panels.
        /// </summary>
        /// <value>The rectangle representing the unused area.</value>
        public Rect ContentRect {
            get { return _contentRect; }
            set { _contentRect = value; }
        }

        /// <summary>
        /// Gets or sets the focused docking window group.
        /// </summary>
        /// <value>The focused window group.</value>
        public DockWindowGroup FocusedWindowGroup {
            get { return (DockWindowGroup)GetValue(FocusedWindowGroupProperty); }
            set { SetValue(FocusedWindowGroupProperty, value); }
        }

        internal DockOperationService DockService {
            get { return _dockService; }
        }
        #endregion

        #region Private Methods
        private void ScaleChildren(double ratio, bool horizontal) {
            List<FrameworkElement> elements = _splitPanels
                .Where(x => horizontal ? (DockManager.GetDock(x) == Dock.Left || DockManager.GetDock(x) == Dock.Right) :
                                         (DockManager.GetDock(x) == Dock.Top || DockManager.GetDock(x) == Dock.Bottom))
                .ToList();

            foreach (FrameworkElement element in elements) {
                double size = DockManager.GetCellSize(element);
                DockManager.SetCellSize(element, Math.Max(size * ratio, MinimumAreaSize));
            }
        }

        private void ResynchronizeChildren() {
            _resynchronizing = true;
            foreach (DockManagerSplitter splitter in _splitters)
                RemoveVisualChild(splitter);

            _collapsedChildren = 0;

            List<DockManagerSplitter> splitterList = new List<DockManagerSplitter>();
            foreach (SplitPanel panel in _splitPanels) {
                if (panel.Visibility != Visibility.Visible) {
                    _collapsedChildren++;
                    continue;
                }

                DockManagerSplitter splitter = new DockManagerSplitter(panel, this);
                
                splitterList.Add(splitter);
                AddVisualChild(splitter);
            }

            _splitters = splitterList.ToArray();
            _isDirty = false;
            _resynchronizing = false;
        }
        #endregion

        #region Protected Methods
        protected virtual void OnFocusedWindowGroupChanged(EventArgs e) {
            if (FocusedWindowGroupChanged != null)
                FocusedWindowGroupChanged(this, e);
        }
        #endregion

        #region Internal Methods
        internal void NotifyGroupFocused(DockWindowGroup grp) {
            FocusedWindowGroup = grp;
            OnFocusedWindowGroupChanged(EventArgs.Empty);
        }

        internal void RegisterFloatingWindow(FloatingWindow window) {
            _floatingWindows.Add(window);
        }

        internal void UnregisterFloatingWindow(FloatingWindow window) {
            _floatingWindows.Remove(window);
        }
        #endregion

        #region Overrides
        /// <summary>
        /// Gets an enumerator for logical child elements of this element.
        /// </summary>
        /// <value></value>
        /// <returns>An enumerator for logical child elements of this element.</returns>
        protected override System.Collections.IEnumerator LogicalChildren {
            get {
                return _splitPanels.GetEnumerator();
            }
        }

        /// <summary>
        /// Overrides <see cref="M:System.Windows.Media.Visual.GetVisualChild(System.Int32)"/>, and returns a child
        /// at the specified index from a collection of child elements.
        /// </summary>
        /// <param name="index">The zero-based index of the requested child element in the collection.</param>
        /// <returns>
        /// The requested child element. This should not return null; if the provided index is out of range, an
        /// exception is thrown.
        /// </returns>
        protected override Visual GetVisualChild(int index) {
            if (index < 0 || index >= VisualChildrenCount)
                throw new ArgumentOutOfRangeException("index");

            if (index == 0) return _backgroundRect;

            index--;

            if (index < _splitPanels.Count) {
                return _splitPanels[index];
            } else {
                return _splitters[index - _splitPanels.Count];
            }
        }

        protected override int VisualChildrenCount {
            get {
                return _splitPanels.Count * 2 + 1 - _collapsedChildren;
            }
        }

        /// <summary>
        /// Called to remeasure a control.
        /// </summary>
        /// <param name="constraint">The maximum size that the method can return.</param>
        /// <returns>
        /// The size of the control, up to the maximum specified by <paramref name="constraint"/>.
        /// </returns>
        protected override Size MeasureOverride(Size constraint) {
            Size availableSize = constraint;
            Size finalSize = new Size(0, 0);

            if (_isDirty && !_resynchronizing) ResynchronizeChildren();

            _backgroundRect.Measure(constraint);
            if (double.IsPositiveInfinity(constraint.Width)) availableSize.Width = 0;
            if (double.IsPositiveInfinity(constraint.Height)) availableSize.Height = 0;

            foreach (SplitPanel panel in _splitPanels) {
                if (panel.Visibility != Visibility.Visible)
                    continue;

                Dock dock = GetDock(panel);
                double size = GetCellSize(panel);
                panel.ClipToBounds = true;

                if (dock == Dock.Left || dock == Dock.Right) {
                    panel.Measure(new Size(size, availableSize.Height));
                    finalSize.Width += size + SplitterWidth;
                    availableSize.Width = Math.Max(0, availableSize.Width - (size + SplitterWidth));
                } else {
                    panel.Measure(new Size(availableSize.Width, size));
                    finalSize.Height += size + SplitterWidth;
                    availableSize.Height = Math.Max(0, availableSize.Height - (size + SplitterWidth));
                }
            }

            finalSize.Width += MinimumAreaSize;
            finalSize.Height += MinimumAreaSize;

            if (finalSize.Width > constraint.Width) {
                ScaleChildren(constraint.Width / finalSize.Width, true);
            }

            if (finalSize.Height > constraint.Height) {
                ScaleChildren(constraint.Height / finalSize.Height, false);
            }

            return new Size(
                double.IsPositiveInfinity(constraint.Width) ? finalSize.Width : constraint.Width,
                double.IsPositiveInfinity(constraint.Height) ? finalSize.Height : constraint.Height
            );
        }

        /// <summary>
        /// Called to arrange and size the content of a <see cref="T:System.Windows.Controls.Control"/> object.
        /// </summary>
        /// <param name="arrangeBounds">The computed size that is used to arrange the content.</param>
        /// <returns>The size of the control.</returns>
        protected override Size ArrangeOverride(Size arrangeBounds) {
            double x = 0.0, y = 0.0;
            double w = arrangeBounds.Width, h = arrangeBounds.Height;
            int i = 0;

            foreach (SplitPanel panel in _splitPanels) {
                if (panel.Visibility != Visibility.Visible)
                    continue;

                double size = GetCellSize(panel);
                Rect finalRect = Rect.Empty;
                Rect splitterRect = Rect.Empty;
                
                switch (GetDock(panel)) {
                    case Dock.Left:
                        finalRect = new Rect(x, y, size, h);
                        splitterRect = new Rect(finalRect.Right, y, SplitterWidth, h);

                        x += finalRect.Width + SplitterWidth;
                        w -= finalRect.Width + SplitterWidth;

                        break;
                    case Dock.Right:
                        double tw = w - (size + SplitterWidth);
                        if (tw < MinimumAreaSize) w += (MinimumAreaSize - tw);

                        finalRect = new Rect(x + w - size, y, size, h);
                        splitterRect = new Rect(finalRect.Left - SplitterWidth, y, SplitterWidth, h);

                        w -= finalRect.Width + SplitterWidth;

                        break;
                    case Dock.Top:
                        finalRect = new Rect(x, y, w, size);
                        splitterRect = new Rect(x, finalRect.Bottom, w, SplitterWidth);

                        y += finalRect.Height + SplitterWidth;
                        h -= finalRect.Height + SplitterWidth;

                        break;
                    case Dock.Bottom:
                        double th = h - (size + SplitterWidth);
                        if (th < MinimumAreaSize) h += (MinimumAreaSize - th);

                        finalRect = new Rect(x, y + h - size, w, size);
                        splitterRect = new Rect(x, finalRect.Top - SplitterWidth, w, SplitterWidth);

                        h -= finalRect.Height + SplitterWidth;

                        break;
                }

                w = Math.Max(w, 0);
                h = Math.Max(h, 0);

                panel.Arrange(finalRect);
                _splitters[i++].Arrange(splitterRect);
            }

            _contentRect = new Rect(x, y, w, h);

            if (_childElement == null)
                _backgroundRect.Arrange(_contentRect);

            return arrangeBounds;
        }

        protected override void OnInitialized(EventArgs e) {
            base.OnInitialized(e);
            ResynchronizeChildren();
        }
        #endregion

        #region IFrameworkElementHost Members
        void IFrameworkElementHost.AddChild(FrameworkElement element) {
            AddVisualChild(element);
            AddLogicalChild(element);
            _isDirty = true;
            InvalidateMeasure();
        }

        void IFrameworkElementHost.RemoveChild(FrameworkElement element) {
            RemoveVisualChild(element);
            RemoveLogicalChild(element);
            _isDirty = true;
            InvalidateMeasure();
        }
        #endregion

        #region IDockHierarchyObject Members
         DockManager IDockHierarchyObject.GetDockManager() {
            return this;
        }

        IDropTarget[] IDockHierarchyObject.FindDropTargets() {
            List<IDropTarget> targets = new List<IDropTarget>();
            foreach (SplitPanel p in _splitPanels) {
                targets.AddRange(((IDockHierarchyObjectHost)p).FindDropTargets());
            }

            foreach (FloatingWindow f in _floatingWindows) {
                targets.AddRange(((IDockHierarchyObjectHost)f).FindDropTargets());
            }

            return targets.ToArray();
        }

        void IDockHierarchyObjectHost.MakeVisible(bool show) {
            Visibility = show ? Visibility.Visible : Visibility.Collapsed;
        }

        void IDockHierarchyObjectHost.MakeDirty() {
            _isDirty = true;
        }

        void IDockHierarchyObjectHost.RemoveChild(FrameworkElement element) {
            if (!(element is SplitPanel)) throw new InvalidOperationException();
            _splitPanels.Remove(element as SplitPanel);
        }
        #endregion
        
        #region IDropTarget Members
        private DockManagerOverlayAdorner _overlay = null;
        private DockHintAdorner _hint = null;
        private DockOverlayHitTestResult _result = DockOverlayHitTestResult.None;

        private void RemoveOverlay() {
            if (_overlay != null) {
                AdornerLayer layer = VisualTreeHelper.GetParent(_overlay) as AdornerLayer;
                if (layer != null)
                    layer.Remove(_overlay);
                _overlay = null;
            }
        }

        private void RemoveHint() {
            if (_hint != null) {
                AdornerLayer layer = VisualTreeHelper.GetParent(_hint) as AdornerLayer;
                if (layer != null)
                    layer.Remove(_hint);
                _hint = null;
            }
        }

        bool IDropTarget.Update(FloatingWindow window, Point mousePos, bool disable) {
            if (this.PointInsideElement(mousePos)) {
                if (_overlay == null) {
                    AdornerLayer layer = AdornerLayer.GetAdornerLayer(this);
                    _overlay = new DockManagerOverlayAdorner(this);
                    layer.Add(_overlay);
                }

                DockOverlayHitTestResult newResult = _overlay.DockOverlay.DockHitTest(mousePos);
                if (newResult != DockOverlayHitTestResult.None) {
                    if (newResult != _result && !disable) {
                        _result = newResult;
                        AdornerLayer layer = AdornerLayer.GetAdornerLayer(this);

                        double maxSize = (_result == DockOverlayHitTestResult.Left || _result == DockOverlayHitTestResult.Right) ?
                            ActualWidth / 2 : ActualHeight / 2;
                        double proposedSize = window.Panel.GetDockSize();

                        _hint = new DockHintAdorner(this, _result, Math.Min(maxSize, proposedSize));
                        layer.Add(_hint);

                        return true;
                    }

                    return !disable;
                } else {
                    _result = newResult;
                    RemoveHint();
                    return false;
                }
            } else {
                RemoveHint();
                RemoveOverlay();

                return false;
            }            
        }

        void IDropTarget.PerformDock(FloatingWindow window, Point mousePos) {
            SplitPanel insertPanel;
            ((IDockHierarchyObjectHost)window).RemoveChild(window.Panel);
            window.Panel.Floating = false;

            Orientation dockOrientation = (_result == DockOverlayHitTestResult.Left || _result == DockOverlayHitTestResult.Right) ?
                Orientation.Horizontal : Orientation.Vertical;

            if (dockOrientation == window.Panel.Orientation) {
                insertPanel = new SplitPanel();
                insertPanel.Children.Add(window.Panel);
            } else {
                insertPanel = window.Panel;
            }

            _splitPanels.Insert(0, insertPanel);

            SetDock(insertPanel,
                _result == DockOverlayHitTestResult.Left ? Dock.Left :
                _result == DockOverlayHitTestResult.Top ? Dock.Top :
                _result == DockOverlayHitTestResult.Right ? Dock.Right :
                _result == DockOverlayHitTestResult.Bottom ? Dock.Bottom :
                    Dock.Left /* Should not be the case */ );

            if (_result == DockOverlayHitTestResult.Left || _result == DockOverlayHitTestResult.Right)
                SetCellSize(insertPanel, Math.Min(ActualWidth / 2, insertPanel.GetDockSize()));
            else
                SetCellSize(insertPanel, Math.Min(ActualHeight / 2, insertPanel.GetDockSize()));
        }

        void IDropTarget.End(FloatingWindow window) {
            RemoveHint();
            RemoveOverlay();
        }
        #endregion
    }
}
